#!/usr/bin/env python
"""
Shadowrun Fourth Edition character tools module.


Code license:

The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 
License for the specific language governing rights and limitations under 
the License.

The Original Code is copyright 2009 OCSrun.

The Initial Developer of the Original Code is OCSrun. All Rights Reserved.

Contributor(s): Brian C. Young, Richard Garfinkel.


Content license: Copyright 2001-2008 WizKids, Inc.

WizKids, Inc. has sole ownership of the names, logo, artwork, marks,
photographs, sounds, audio, video and/or any proprietary material used in
connection with the game Shadowrun. WizKids, Inc. has granted permission to
OCSrun to use such names, logos, artwork, marks and/or any proprietary
materials for promotional and informational purposes on its website but does
not endorse, and is not affiliated with OCSrun in any official capacity
whatsoever.
"""

from utils import *
from wireless import *
from pickle import Pickler, Unpickler
import os

class initiativeMode:
    """
    Kind of an enum class, for different ways a character's initiative
    score can be calculated.
    """
    normal = "INITIATIVE_MODE_NORMAL"
    astral = "INITIATIVE_MODE_ASTRAL"
    matrix = "INITIATIVE_MODE_MATRIX"

class specialAttrQualities:
    """
    Kind of an enum class, for character qualities that are mutually
    exclusive and unlock special attributes.
    """
    mundane = "CHARACTER_STATE_MUNDANE"
    magician = "CHARACTER_STATE_MAGICIAN"
    adept = "CHARACTER_STATE_MYSTIC_ADEPT"
    technomancer = "CHARACTER_STATE_TECHNOMANCER"

class SRCharacter:
    """
    A class representing a single Shadowrun character.
    Currently for initiative purposes only.

    Public members (read/write):
        mName - string - character's name
        Attributes (each integer):
            - mStrength
            - mAgility
            - mReaction
            - mBody
            - mCharisma
            - mLogic
            - mIntuition
            - mWillpower
            - mEdge
            - mEssence
            - mMagic
            - mResonance
        mInitMode - string - a member of the initiativeMode class. One of
            normal, astral, coldsim, or hotsim.
        mExtraInitPasses - integer - number of extra initiative passes the
            character gets, whether from bioware, magic, or whatever
        mStunDamage - integer - number of boxes of stun damage taken
        mPhysDamage - integer - number of boxes of physical damage 
            taken
        mComm - Commlink - the commlink that the character is currently using.
            Do not set if the character is a technomancer.
            
        mPainTolerance - integer - rating of pain tolerance
        mLowPain - integer - 1 if character has the Low Pain Tolerance 
            Quality, 0 otherwise
        mSpecialAttrState - a member of the specialAttrQualities class.
            Determines if Magic, Resonance, and astral init are available.
        
    Public members (read-only please):
        mCurrentInitPasses - integer - number of initiative passes in this
            combat turn. UNINITIALIZED until rollNewInit is called
    Private members (on the honor code):
        mInitDice   - integer - dice to roll for initiative
        mInitScore  - integer - current initiative score. UNINITIALIZED 
            until rollNewInit is called.  -1 indicates a critical glitch on the
            initiative test, and the character should act at end of round.
        mOrigComm - Commlink - used for preserving the character's commlink when
            the technomancer quality is set and unset.
    """

    def __init__(self, name):
        """
        Create a character with the given name and default values.
        """
        self.mName = name

        self.mStrength = 1
        self.mAgility = 1
        self.mReaction = 1
        self.mBody = 1
        self.mCharisma = 1
        self.mLogic = 1
        self.mIntuition = 1
        self.mWillpower = 1
        self.mEdge = 1
        self.mEssence = 6
        self.mMagic = 0
        self.mResonance = 0
        
        self.mInitMode = initiativeMode.normal
        self.mExtraInitPasses = 0

        self.mComm = Commlink()
        self.mOrigComm = None
        
        self.mStunDamage = 0
        self.mPhysDamage = 0
        self.mPainTolerance = 0
        self.mLowPain = 0
        self.mSpecialAttrState = specialAttrQualities.mundane

    def doDamage(self, phys=0, stun=0):
        """
        Apply damage (physical, stun, or both) to a character. Alternately,
        use negative numbers to heal a character.
        """
        self.mPhysDamage = max (0, self.mPhysDamage + phys)
        self.mStunDamage = max (0, self.mStunDamage + stun)

    def getDamagePenalty(self):
        """
        Return the character's current dice pool penalty for damage.
        Positive numbers are a larger penalty.
        """
        woundDiv = 3
        if self.mLowPain:
             woundDiv = 2
        penalty = self.mStunDamage // 3 + \
                  max(0, self.mPhysDamage - self.mPainTolerance) // woundDiv
        return penalty

    def rollNewInit(self):
        """
        Make the appropriate rolls for a new combat round, and initialize
        all variables that the initiative tool will use.
        """
        (hits, glitch, crit) = rollDice(self.getInitDice())
        self.mInitScore = hits
        self.mCurrentInitPasses = self.getInitPasses()
        if glitch:
            self.mInitScore -= 0.1
        if crit:
            self.mInitScore = -1
            if self.mCurrentInitPasses > 1:
                self.mCurrentInitPasses -= 1

    def getInitPasses(self):
        """
        Return the number of initiative passes that a character gets.
        """
        if self.mInitMode == initiativeMode.normal:
            return 1 + self.mExtraInitPasses
        elif self.mInitMode == initiativeMode.astral:
            return 3 + self.mExtraInitPasses
        elif self.mInitMode == initiativeMode.matrix:
            return self.mComm.getInitPasses() + self.mExtraInitPasses
        raise ValueError("Unknown initiative mode")
    
    def getInitDice(self):
        """
        Return the number of dice a character should roll for initiative.
        """
        if self.mInitMode == initiativeMode.normal:
            return self.mIntuition + self.mReaction - self.getDamagePenalty()
        if self.mInitMode == initiativeMode.astral:
            return self.mIntuition + self.mIntuition - self.getDamagePenalty()
        if self.mInitMode == initiativeMode.matrix:
            return self.mIntuition + self.mComm.getResponse() - self.getDamagePenalty()
        raise ValueError("Unknown initiative mode")
    
    def getInitScore(self):
        """
        Return the character's current initiative score, as modified for
        damage.
        """
        if self.mInitScore == -1:
            return 0
        return self.getInitDice() + self.mInitScore

    def initOrderComparator(self, right):
        """
        Between two characters, determine which one goes first in the
        current initiative round.  Use with list.sort().
          
        Full priority order is:
            Initiative score (minus damage penalty)
            Edge stat
            Initiative attribute
            Reaction stat
        """
        if self.mInitScore == -1:
            if right.mInitScore == -1:
                return self.initOrderTiebreaker(right)
            return 1
        if right.mInitScore == -1:
            return -1
        if self.getInitScore() < right.getInitScore():
            return 1
        if self.getInitScore() > right.getInitScore():
            return -1
        return self.initOrderTiebreaker(right)
    
    def initOrderTiebreaker(self, right):
        """
        Between two characters, break initiative ties.  This functions is
        separate from initOrderComparator for use at the end of an initiative
        pass.
        """
        if self.mEdge < right.mEdge:
            return 1
        if self.mEdge > right.mEdge:
            return -1
        if self.getInitDice() < right.getInitDice():
            return 1
        if self.getInitDice() > right.getInitDice():
            return -1
        if self.mReaction < right.mReaction:
            return 1
        if self.mReaction > right.mReaction:
            return -1
        return 0

    def saveToFile(self, fileOut):
        """
        Write this character out to a file object.

        This uses the pickle module, which is apparently a security
        vulnerability.  This is temporary, and will hopefully be replaced
        by a schema-validated XML format.
        """
        Pickler(fileOut, 0).dump(self)

def loadSRCharacter(fileIn):
    """
    Load an SRCharacter object from a file object.

    This uses the pickle module, which is apparently a security
    vulnerability.  This is temporary, and will hopefully be replaced
    by a schema-validated XML format.
    """
    # TODO: Error handling?
    char = Unpickler(fileIn).load()
    return char

########## 
# TKinter GUI
##########

from Tkinter import *
import tkFileDialog

class CharacterEditorFrame(Frame):
    """
    A widget class for editing values in a character.
    """
    def __init__(self, master, char):
        Frame.__init__(self, master, pady=2, padx=2)
        self.mChar = char

        self.mNameVar = StringVar(self, char.mName)
        self.mNameVar.trace("w", self.editcb)

        self.mStrengthVar  = ValidatingIntVar(self, self.mChar.mStrength)
        self.mAgilityVar   = ValidatingIntVar(self, self.mChar.mAgility)
        self.mReactionVar  = ValidatingIntVar(self, self.mChar.mReaction)
        self.mBodyVar      = ValidatingIntVar(self, self.mChar.mBody)
        self.mCharismaVar  = ValidatingIntVar(self, self.mChar.mCharisma)
        self.mLogicVar     = ValidatingIntVar(self, self.mChar.mLogic)
        self.mIntuitionVar = ValidatingIntVar(self, self.mChar.mIntuition)
        self.mWillpowerVar = ValidatingIntVar(self, self.mChar.mWillpower)
        self.mEssenceVar   = ValidatingIntVar(self, self.mChar.mEssence)
        self.mMagicVar     = ValidatingIntVar(self, self.mChar.mMagic)
        self.mResonanceVar = ValidatingIntVar(self, self.mChar.mResonance)
        self.mEdgeVar      = ValidatingIntVar(self, self.mChar.mEdge)

        self.mPassVar = ValidatingIntVar(self, char.mExtraInitPasses)
        self.mPhysVar = ValidatingIntVar(self, char.mPhysDamage)
        self.mStunVar = ValidatingIntVar(self, char.mStunDamage)
        self.mInitModeVar = StringVar(self, char.mInitMode)
        
        self.mPainVar = IntVar(self, char.mPainTolerance)
        self.mPainVar.trace("w", self.editcb)
        self.mLowVar = IntVar(self, char.mLowPain)
        self.mLowVar.trace("w", self.editcb)
        self.mSpecialAttrVar = StringVar(self, char.mSpecialAttrState)

        nameField = Entry(self, width=32, textvariable=self.mNameVar)
        nameField.pack(fill=X)

        # Attributes
        attrframe = LabelFrame(self, text="Attributes")
        leftframe = Frame(attrframe)
        midframe = Frame(attrframe)
        rightframe = Frame(attrframe)
        LabeledNumberEntry(leftframe, "Strength:", 15,
                           self.mStrengthVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(leftframe, "Agility:", 15,
                           self.mAgilityVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(leftframe, "Reaction:", 15,
                           self.mReactionVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(leftframe, "Body:", 15,
                           self.mBodyVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(midframe, "Charisma:", 15,
                           self.mCharismaVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(midframe, "Intuition:", 15,
                           self.mIntuitionVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(midframe, "Logic:", 15,
                           self.mLogicVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(midframe, "Willpower:", 15,
                           self.mWillpowerVar, 1, 9,
                           self.attrcb, True).pack()
        LabeledNumberEntry(rightframe, "Edge:", 15,
                           self.mEdgeVar, 1, 7,
                           self.attrcb, True).pack()
        LabeledNumberEntry(rightframe, "Essence:", 15,
                           self.mEssenceVar, 0, 6,
                           self.attrcb, True).pack()
        self.mMagicEntry = \
            LabeledNumberEntry(rightframe, "Magic:", 15,
                               self.mMagicVar, 0, 6,
                               self.attrcb, True)
        self.mMagicEntry.setEnabled(False)
        self.mMagicEntry.pack()
        self.mResonanceEntry = \
            LabeledNumberEntry(rightframe, "Resonance:", 15,
                               self.mResonanceVar, 0, 6,
                               self.attrcb, True)
        self.mResonanceEntry.setEnabled(False)
        self.mResonanceEntry.pack()
        
        leftframe.pack(side=LEFT)
        midframe.pack(side=LEFT)
        rightframe.pack(side=LEFT)
        attrframe.pack()

        midframe = Frame(self)
        leftframe = Frame(midframe)
        rightframe = Frame(midframe)

        # Commlink
        commlabel = LabelFrame(leftframe, text="Commlink")
        self.mCommFrame = CommlinkEditorFrame(commlabel, self.mChar.mComm)
        self.mCommFrame.pack()
        commlabel.pack()

        # Initiative
        initframe = LabelFrame(leftframe, text="Initiative")
        LabeledNumberEntry(initframe, "Bonus Passes:", 15,
                           self.mPassVar, 0, 3,
                           self.editcb, True).pack()
        Radiobutton(initframe, text="Normal init", variable=self.mInitModeVar,
                    value=initiativeMode.normal,
                    command=self.initcb).pack(anchor=W)
        self.mAstralInitEntry = \
            Radiobutton(initframe, text="Astral init",
                        variable=self.mInitModeVar,
                        value=initiativeMode.astral,
                        command=self.initcb)
        self.mAstralInitEntry.config(state=DISABLED)
        self.mAstralInitEntry.pack(anchor=W)
        Radiobutton(initframe, text="Matrix init", variable=self.mInitModeVar,
                    value=initiativeMode.matrix,
                    command=self.initcb).pack(anchor=W)
        initframe.pack()
        
        # Other editables
        LabeledNumberEntry(rightframe, "Physical Damage:", 15,
                           self.mPhysVar, 0, 12,
                           self.editcb, True).pack()
        LabeledNumberEntry(rightframe, "Stun Damage:", 15,
                           self.mStunVar, 0, 12,
                           self.editcb, True).pack()

        # Qualities
        qualityframe = LabelFrame(rightframe, text="Qualities")
        Scale(qualityframe, from_=0, to=3, resolution=1,
              label="Pain Tolerance", variable = self.mPainVar,
              orient=HORIZONTAL).pack()
        Checkbutton(qualityframe, text="Low Pain Tolerance",
                    variable = self.mLowVar).pack()
        Radiobutton(qualityframe, text="Mundane",
                    variable=self.mSpecialAttrVar,
                    value=specialAttrQualities.mundane,
                    command=self.specialcb).pack(anchor=W)
        Radiobutton(qualityframe, text="Magician",
                    variable=self.mSpecialAttrVar,
                    value=specialAttrQualities.magician,
                    command=self.specialcb).pack(anchor=W)
        Radiobutton(qualityframe, text="Mystic Adept",
                    variable=self.mSpecialAttrVar,
                    value=specialAttrQualities.adept,
                    command=self.specialcb).pack(anchor=W)
        Radiobutton(qualityframe, text="Technomancer",
                    variable=self.mSpecialAttrVar,
                    value=specialAttrQualities.technomancer,
                    command=self.specialcb).pack(anchor=W)

        qualityframe.pack()

        leftframe.pack(side=LEFT, anchor=N)
        rightframe.pack(side=LEFT, anchor=N)
        midframe.pack()

        # Make sure the right fields are enabled
        self.specialcb()

    def editcb(self, caller, index, mode):
        """
        Trace callback for each of the field entries.  When this is called,
        update the appropriate field in the SRCharacter.

        There has got to be a better way to do this...
        """
        char = self.mChar
        if caller == str(self.mNameVar):
            char.mName = self.mNameVar.get()
            
        elif caller == str(self.mPassVar):
            char.mExtraInitPasses = self.mPassVar.get()
        elif caller == str(self.mPhysVar):
            char.mPhysDamage = self.mPhysVar.get()
        elif caller == str(self.mStunVar):
            char.mStunDamage = self.mStunVar.get()
        elif caller == str(self.mPainVar):
            char.mPainTolerance = self.mPainVar.get()
        elif caller == str(self.mLowVar):
            char.mLowPain = self.mLowVar.get()
        else:
            raise ValueError("Unknown caller " + caler)

    def attrcb(self, caller, index, mode):
        """
        Trace callback for attributes.
        """
        char = self.mChar
        if caller == str(self.mStrengthVar):
            char.mStrength = self.mStrengthVar.get()
        elif caller == str(self.mAgilityVar):
            char.mAgility = self.mAgilityVar.get()
        elif caller == str(self.mReactionVar):
            char.mReaction = self.mReactionVar.get()
        elif caller == str(self.mBodyVar):
            char.mBody = self.mBodyVar.get()
            
        elif caller == str(self.mCharismaVar):
            char.mCharisma = self.mCharismaVar.get()
        elif caller == str(self.mLogicVar):
            char.mLogic = self.mLogicVar.get()
        elif caller == str(self.mIntuitionVar):
            char.mIntuition = self.mIntuitionVar.get()
        elif caller == str(self.mWillpowerVar):
            char.mWillpower = self.mWillpowerVar.get()
            
        elif caller == str(self.mEdgeVar):
            char.mEdge = self.mEdgeVar.get()
        elif caller == str(self.mEssenceVar):
            char.mEssence = self.mEssenceVar.get()
        elif caller == str(self.mMagicVar):
            char.mMagic = self.mMagicVar.get()
        elif caller == str(self.mResonanceVar):
            char.mResonance = self.mResonanceVar.get()

        else:
            raise ValueError("Unknown caller " + caller)

    def initcb(self):
        """
        Trace callback for initiative mode changes
        """
        self.mChar.mInitMode = self.mInitModeVar.get()
    
    def specialcb(self):
        """
        Trace callback for special attribute state (mundane, awakened,
        technomancer) changes
        """
        self.mChar.mSpecialAttrState = self.mSpecialAttrVar.get()

        state = self.mChar.mSpecialAttrState
        if state == specialAttrQualities.mundane:
            self.mMagicVar.set(0)
            self.mMagicEntry.setEnabled(False)
            self.mAstralInitEntry.config(state=DISABLED)
            self.mResonanceVar.set(0)
            self.mResonanceEntry.setEnabled(False)
            if self.mChar.mOrigComm != None:
                self.mChar.mComm = self.mChar.mOrigComm
                self.mChar.mOrigComm = None
                self.mCommFrame.mComm = self.mChar.mComm
            self.mCommFrame.setEnabled(True)
        elif state == specialAttrQualities.magician or \
             state == specialAttrQualities.adept:
            self.mMagicEntry.setEnabled(True)
            self.mAstralInitEntry.config(state=NORMAL)
            self.mResonanceVar.set(0)
            self.mResonanceEntry.setEnabled(False)
            if self.mChar.mOrigComm != None:
                self.mChar.mComm = self.mChar.mOrigComm
                self.mChar.mOrigComm = None
                self.mCommFrame.mComm = self.mChar.mComm
            self.mCommFrame.setEnabled(True)
        elif state == specialAttrQualities.technomancer:
            self.mMagicVar.set(0)
            self.mMagicEntry.setEnabled(False)
            self.mAstralInitEntry.config(state=DISABLED)
            self.mResonanceEntry.setEnabled(True)
            if self.mChar.mOrigComm == None:
                self.mChar.mOrigComm = self.mChar.mComm
                self.mChar.mComm = TechnomancyCommlink(self.mChar)
                self.mCommFrame.mComm = self.mChar.mComm
            self.mCommFrame.setEnabled(False)
        else:
            raise ValueError("Unknown special character type " + state)

if __name__ == "__main__":
    root = Tk()
    def updateTitle(filename):
        global root
        if filename == None or filename == '':
            root.wm_title("Shadowrun 4 Character Editor")
            return
        root.wm_title("Shadowrun 4 Character Editor: " +
                      filename.split(os.sep)[-1].split(os.altsep)[-1])
    root.wm_resizable(False, False)
    updateTitle(None)

    char = SRCharacter("Dude")
    charEdit = CharacterEditorFrame(root, char)
    charEdit.pack()

    filename = None

    #TODO: Error catching on these
    def saveAs():
        global filename
        newfilename = tkFileDialog.asksaveasfilename(
            defaultextension = ".s4c",
            filetypes = [("Sr4toolkit character files", "*.s4c"),
                         ("All files", "*.*")],
            parent = root,
            title = "Save Character As...")
        if newfilename == '':
            return
        filename = newfilename
        updateTitle(filename)
        save()
    def save():
        global filename, char
        if filename == None:
            saveAs()
            return
        fileOut = open(filename, "w")
        char.saveToFile(fileOut)
        fileOut.close()
    def load():
        global filename, charEdit, char
        newfilename = tkFileDialog.askopenfilename(
            defaultextension = ".s4c",
            filetypes = [("Sr4toolkit character files", "*.s4c"),
                         ("All files", "*.*")],
            parent = root,
            title = "Open Character...")
        if newfilename == '':
            return
        filename = newfilename
        fileIn = open(filename, "r")
        char = loadSRCharacter(fileIn)
        fileIn.close()
        charEdit.destroy()
        charEdit = CharacterEditorFrame(root, char)
        charEdit.pack()
        updateTitle(filename)

    menubar = Menu(root)
    filemenu = Menu(menubar, tearoff=0)
    filemenu.add_command(label="Open...", command=load)
    filemenu.add_command(label="Save", command=save)
    filemenu.add_command(label="Save As...", command=saveAs)
    menubar.add_cascade(label="File", menu=filemenu)
    helpmenu = Menu(menubar, tearoff=0)
    helpmenu.add_command(label="About...",
                         command=lambda: showAboutWindow(root))
    menubar.add_cascade(label="Help", menu=helpmenu)
    root.config(menu=menubar)
    
    root.mainloop()
